package it.i118.jindice.json;

import it.i118.jindice.DataObject;

import java.io.*;
import java.lang.reflect.*;
import java.util.Enumeration;
import java.util.HashMap;

/**
 * Created with IntelliJ IDEA.
 * User: sciovante
 * Date: 04/11/13
 * Time: 16.20
 */
public class JSON {
    public JSON() {
        setupDefaultTypeAdapter();
    }

    public String toJson(DataObject bean) {
        StringWriter writer = new StringWriter();
        try {
            toJson(bean, writer);
        } catch (IOException e) {
            new RuntimeException(e);
        }
        return writer.toString();
    }

    public void toJson(DataObject bean, Writer writer) throws IOException {

        JSONWriter jsonWriter = new JSONWriter(writer);
        jsonWriter.object();

        Class beanClass = bean.getClass();

        while (true) {
            Field[] fields = beanClass.getDeclaredFields();
            for (Field field : fields) {
                writeField(field, bean, jsonWriter);

            }
            if (beanClass.equals(DataObject.class)) {
                break;
            }
            beanClass = beanClass.getSuperclass();
        }
        jsonWriter.endObject();
    }


    private void writeField(Field field, Object bean, JSONWriter jsonWriter) throws IOException {
        System.out.println("JSON.writeField - field = " + field);
        boolean isAccessible = field.isAccessible();

        try {
            field.setAccessible(true);
            Class type = field.getType();
            System.out.println("JSON.writeField - type = " + type);
            String name = field.getName();
            Object value = field.get(bean);

            jsonWriter.key(name);

            if (value == null) {
                jsonWriter.value(null);

            } else if (type.isArray()) {
                jsonWriter.array();
                writeArray(field, value, jsonWriter);
                jsonWriter.endArray();

            } else {
                writeValue(field, value, jsonWriter);

            }
        } catch (IllegalAccessException e) {
            throw new JSONException(e);
        } finally {
            field.setAccessible(isAccessible);
        }
    }

    private void writeValue(Field field, Object value, JSONWriter jsonWriter) throws IOException {

        Class type = field.getType();

        if (type.isEnum()) {
            jsonWriter.value(value);

        } else if (type.isPrimitive()) {
            jsonWriter.value(value);

        } else if (isBoxing(type)) {
            jsonWriter.value(value);

        } else {

            TypeAdapter typeAdapter = getTypeAdapter(type);
            if (typeAdapter != null) {
                String string = typeAdapter.toJson(value);
                jsonWriter.value(string);
            } else {
                throw new JSONException("Tipo '" + type + "' non supportato");
            }
        }
    }

    private void writeArray(Field field, Object array, JSONWriter jsonWriter) throws IOException {

        Class type = field.getType();
        Class componentType = type.getComponentType();
//        System.out.println("Converter.writeArray - componentType = " + componentType);

        int length = Array.getLength(array);
        if (componentType.isEnum()) {
            for (int i = 0; i < length; i++) {
                Object value = Array.get(array, i);
                jsonWriter.value(value);
            }

        } else if (componentType.isPrimitive()) {
            for (int i = 0; i < length; i++) {
                Object value = Array.get(array, i);
                jsonWriter.value(value);
            }

        } else if (isBoxing(componentType)) {
            for (int i = 0; i < length; i++) {
                Object value = Array.get(array, i);
                jsonWriter.value(value);
            }

        } else {
            TypeAdapter typeAdapter = getTypeAdapter(componentType);
            if (typeAdapter != null) {
                for (int i = 0; i < length; i++) {
                    Object value = Array.get(array, i);
                    jsonWriter.value(typeAdapter.toJson(value));
                }
            } else {
                throw new JSONException("componentType '" + componentType + "' non supportato");

            }
        }
    }

    public <T> T fromJson(String json, Class<T> beanClass) {
        return fromJson(new StringReader(json), beanClass);
    }

    public <T> T fromJson(Reader reader, Class<T> beanClass) {

        Class fieldClass = beanClass;
        HashMap<String, Field> fieldMap = new HashMap<String, Field>();

        while (true) {
            Field[] fields = fieldClass.getDeclaredFields();
            for (Field field : fields) {
                fieldMap.put(field.getName(), field);
            }
            if (fieldClass.equals(DataObject.class)) {
                break;
            }
            fieldClass = fieldClass.getSuperclass();
        }

        T target = null;
        try {
            target = beanClass.newInstance();
        } catch (InstantiationException e) {
            throw new JSONException(e);
        } catch (IllegalAccessException e) {
            throw new JSONException(e);
        }

        TokenEnumeration enumeration = new TokenEnumeration(reader);
        while (enumeration.hasMoreElements()) {
            Token token = enumeration.nextElement();
//            System.out.println("Reader.fromJson - token = " + token);
            Field field = fieldMap.get(token.key);
            if (field == null) {
                throw new JSONException("NoSuchFieldException:" + token.key);
            }

            readField(target, field, token.value);
        }
        return target;
    }


    private void readField(Object bean, Field field, Object value) {
        boolean isAccessible = field.isAccessible();

        try {
            field.setAccessible(true);
            Class type = field.getType();

            if (value == JSONObject.NULL) {
                field.set(bean, null);

            } else if (type.isArray()) {
                readArray(bean, field, value);

            } else {
                readValue(bean, field, value);

            }
        } catch (IllegalAccessException iae) {
            throw new JSONException(iae);
        } finally {
            field.setAccessible(isAccessible);
        }
    }

    private void readValue(Object bean, Field field, Object value) throws IllegalAccessException {

        Class type = field.getType();

//        System.out.println("Reader.readValue - value = " + value.getClass());
        if (type.isEnum()) {
//            field.set(bean, value);
//              Persona.Genere
            try {
                Method method = type.getMethod("valueOf", String.class);
                Enum enumValue = (Enum) method.invoke(null, value);
                field.set(bean, enumValue);
            } catch (NoSuchMethodException e) {
                throw new RuntimeException(e);
            } catch (InvocationTargetException e) {
                throw new RuntimeException(e);
            }
        } else if (type.isPrimitive()) {

//            The value can be a Boolean, Double, Integer, JSONArray, JSONObject, Long, or String, or the JSONObject.NULL
            if (Boolean.TYPE.equals(type)) {
                field.set(bean, value);

            } else if (Character.TYPE.equals(type)) {
                field.set(bean, ((String) value).charAt(0));

            } else if (Byte.TYPE.equals(type)) {
                field.set(bean, ((Integer) value).byteValue());

            } else if (Short.TYPE.equals(type)) {
                field.set(bean, ((Integer) value).shortValue());

            } else if (Integer.TYPE.equals(type)) {
                field.set(bean, value);

            } else if (Long.TYPE.equals(type)) {
                if (value instanceof Integer) {
                    field.set(bean, ((Integer) value).longValue());
                } else {
                    field.set(bean, value);
                }

            } else if (Float.TYPE.equals(type)) {
                field.set(bean, ((Double) value).floatValue());

            } else if (Double.TYPE.equals(type)) {
                field.set(bean, value);

            }
        } else if (isBoxing(type)) {
//            The value can be a Boolean, Double, Integer, JSONArray, JSONObject, Long, or String, or the JSONObject.NULL
            if (Boolean.class.equals(type)) {
                field.set(bean, value);

            } else if (Character.class.equals(type)) {
                field.set(bean, ((String) value).charAt(0));

            } else if (Byte.class.equals(type)) {
                field.set(bean, ((Integer) value).byteValue());

            } else if (Short.class.equals(type)) {
                field.set(bean, ((Integer) value).shortValue());

            } else if (Integer.class.equals(type)) {
                field.set(bean, value);

            } else if (Long.class.equals(type)) {

                if (value instanceof Integer) {
                    field.set(bean, ((Integer) value).longValue());
                } else {
                    field.set(bean, value);
                }

            } else if (Float.class.equals(type)) {
                if (value instanceof Integer) {
                    field.set(bean, ((Integer) value).floatValue());
                } else {
                    field.set(bean, ((Double) value).floatValue());
                }

            } else if (Double.class.equals(type)) {
                if (value instanceof Integer) {
                    field.set(bean, ((Integer) value).doubleValue());
                } else {
                    field.set(bean, value);
                }

            } else {
                throw new JSONException("Tipo '" + type + "' non supportato");
            }

        } else {
            TypeAdapter typeAdapter = getTypeAdapter(type);
            if (typeAdapter != null) {
                Object obj = typeAdapter.fromJson((String) value);
                field.set(bean, obj);
            } else {
                throw new JSONException("Tipo '" + type + "' non supportato");
            }
        }
    }

    private void readArray(Object bean, Field field, Object value) throws IllegalAccessException {
        Class type = field.getType();
        Class componentType = type.getComponentType();

        JSONArray jsonArray = (JSONArray) value;

        int length = jsonArray.length();
        if (componentType.isEnum()) {
            Enum[] array = (Enum[]) java.lang.reflect.Array.newInstance(componentType, length);
            try {
                Method method = componentType.getMethod("valueOf", String.class);
                for (int i = 0; i < length; i++) {
                    array[i] = (Enum) method.invoke(null, jsonArray.getString(i));
                }
                field.set(bean, array);
            } catch (NoSuchMethodException e) {
                throw new RuntimeException(e);
            } catch (InvocationTargetException e) {
                throw new RuntimeException(e);
            }


            field.set(bean, array);

        } else if (componentType.isPrimitive()) {
            if (Boolean.TYPE.equals(componentType)) {
                boolean[] array = new boolean[length];
                for (int i = 0; i < length; i++) {
                    array[i] = jsonArray.getBoolean(i);
                }

                field.set(bean, array);

            } else if (Character.TYPE.equals(componentType)) {
                char[] array = new char[length];
                for (int i = 0; i < length; i++) {
                    array[i] = jsonArray.getString(i).charAt(0);
                }

                field.set(bean, array);

            } else if (Byte.TYPE.equals(componentType)) {
                byte[] array = new byte[length];
                for (int i = 0; i < length; i++) {
                    array[i] = (byte) jsonArray.getInt(i);
                }
                field.set(bean, array);

            } else if (Short.TYPE.equals(componentType)) {
                short[] array = new short[length];
                for (int i = 0; i < length; i++) {
                    array[i] = (short) jsonArray.getInt(i);
                }

                field.set(bean, array);

            } else if (Integer.TYPE.equals(componentType)) {
                int[] array = new int[length];
                for (int i = 0; i < length; i++) {
                    array[i] = jsonArray.getInt(i);
                }
                field.set(bean, array);

            } else if (Long.TYPE.equals(componentType)) {
                long[] array = new long[length];
                for (int i = 0; i < length; i++) {
                    array[i] = jsonArray.getLong(i);
                }
                field.set(bean, array);

            } else if (Float.TYPE.equals(componentType)) {
                float[] array = new float[length];
                for (int i = 0; i < length; i++) {
                    array[i] = (float) jsonArray.getDouble(i);
                }
                field.set(bean, array);

            } else if (Double.TYPE.equals(componentType)) {
                double[] array = new double[length];
                for (int i = 0; i < length; i++) {
                    array[i] = jsonArray.getDouble(i);
                }
                field.set(bean, array);
            } else {
                throw new JSONException("Tipo '" + componentType + "' non supportato");
            }

        } else if (isBoxing(componentType)) {

            if (Boolean.class.equals(componentType)) {
                Boolean[] array = new Boolean[length];
                for (int i = 0; i < length; i++) {
                    array[i] = jsonArray.getBoolean(i);
                }

                field.set(bean, array);

            } else if (Character.class.equals(componentType)) {
                Character[] array = new Character[length];
                for (int i = 0; i < length; i++) {
                    array[i] = jsonArray.getString(i).charAt(0);
                }

                field.set(bean, array);

            } else if (Byte.class.equals(componentType)) {
                Byte[] array = new Byte[length];
                for (int i = 0; i < length; i++) {
                    array[i] = (byte) jsonArray.getInt(i);
                }

                field.set(bean, array);

            } else if (Short.class.equals(componentType)) {
                Short[] array = new Short[length];
                for (int i = 0; i < length; i++) {
                    array[i] = (short) jsonArray.getInt(i);
                }

                field.set(bean, array);
            } else if (Integer.class.equals(componentType)) {
                Integer[] array = new Integer[length];
                for (int i = 0; i < length; i++) {
                    array[i] = jsonArray.getInt(i);
                }
                field.set(bean, array);

            } else if (Long.class.equals(componentType)) {
                Long[] array = new Long[length];
                for (int i = 0; i < length; i++) {
                    array[i] = jsonArray.getLong(i);
                }
                field.set(bean, array);

            } else if (Float.class.equals(componentType)) {
                Float[] array = new Float[length];
                for (int i = 0; i < length; i++) {
                    array[i] = (float) jsonArray.getDouble(i);
                }
                field.set(bean, array);
            } else if (Double.class.equals(componentType)) {
                Double[] array = new Double[length];
                for (int i = 0; i < length; i++) {
                    array[i] = jsonArray.getDouble(i);
                }
                field.set(bean, array);
            } else {
                throw new JSONException("Tipo '" + componentType + "' non supportato");
            }

        } else {
            TypeAdapter typeAdapter = getTypeAdapter(componentType);
            if (typeAdapter != null) {
                String[] array = new String[length];
                for (int i = 0; i < length; i++) {
                    array[i] = typeAdapter.toJson(jsonArray.get(i));
                }
                field.set(bean, array);
            } else {
                throw new JSONException("componentType '" + componentType + "' non supportato");

            }
        }
    }

    private boolean isBoxing(Class type) {
        return type.equals(Boolean.class) ||
                type.equals(Character.class) ||
                type.equals(Byte.class) ||
                type.equals(Short.class) ||
                type.equals(Integer.class) ||
                type.equals(Long.class) ||
                type.equals(Float.class) ||
                type.equals(Double.class);
    }

    private class Token {
        final String key;
        //    The value can be a Boolean, Double, Integer, JSONArray, JSONObject, Long, or String, or the JSONObject.NULL
        final Object value;

        public Token(String key, Object value) {
            this.key = key;
            this.value = value;
        }

        @Override
        public String toString() {
            return "Token{" +
                    "key='" + key + '\'' +
                    ", value=" + value +
                    '}';
        }
    }

    private class TokenEnumeration implements Enumeration<Token> {
        JSONTokener tokener;
        Token next = null;

        public TokenEnumeration(String json) {
            this(new StringReader(json));
        }

        public TokenEnumeration(java.io.Reader reader) {
            tokener = new JSONTokener(reader);
            if (tokener.nextClean() != '{') {
                throw tokener.syntaxError("A JSONObject text must begin with '{'");
            }

            readNextToken();
        }

        @Override
        public boolean hasMoreElements() {
            return next != null;
        }

        @Override
        public Token nextElement() {
            try {
                return next;
            } finally {
                readNextToken();
            }
        }

        public void readNextToken() throws JSONException {
            char c;
            String key;
            next = null;

            c = tokener.nextClean();

            switch (c) {
                case 0:
                    throw tokener.syntaxError("A JSONObject text must end with '}'");
                case '}':
                    return;
                default:
                    tokener.back();
                    key = tokener.nextValue().toString();
            }

// The key is followed by ':'.

            c = tokener.nextClean();
            if (c != ':') {
                throw tokener.syntaxError("Expected a ':' after a key");
            }
            Object value = tokener.nextValue();
//        System.out.println("ReaderTest.test - " + key + " = " + value);
            next = new Token(key, value);

// Pairs are separated by ','.

            switch (tokener.nextClean()) {
                case ';':
                case ',':
                    break;
                case '}':
                    tokener.back();
                    return;
                default:
                    throw tokener.syntaxError("Expected a ',' or '}'");
            }
        }
    }

    private HashMap<Type, TypeAdapter> typeAdapterMap = new HashMap<Type, TypeAdapter>();

    public void setTypeAdapter(TypeAdapter<?> typeAdapter) {
        Class<?> persistentClass = (Class<?>) ((ParameterizedType) typeAdapter.getClass().getGenericSuperclass()).getActualTypeArguments()[0];
        typeAdapterMap.put(persistentClass, typeAdapter);
    }

    private TypeAdapter getTypeAdapter(Type type) {
        return typeAdapterMap.get(type);
    }

    private void setupDefaultTypeAdapter() {
        setTypeAdapter(new TypeAdapter<String>() {
            @Override
            public String fromJson(String value) {
                return value;
            }

            @Override
            public String toJson(String value) {
                return value;
            }
        });
    }
}
